# Copyright (C) 2008 Hemant Goyal <goyal.hemant@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import re
import os
from sets import Set

class conf_object:
    '''This class provides a data structure to handle the dot.conf
       parameters, its values, comment status effectively.
    '''

    def __init__(self, parameter, iscommented, value, position=None):
        '''parameter    :       dot.conf Paramater
           iscommented  :       False indicates that the Parameter
                                entry is commented in the dot.conf file.
           value        :       A list of values for the parameter
        '''   
        
        self._parameter = parameter
        self._iscommented = iscommented
        self._position = position
        
        if(str(value.__class__.__name__ ) != "list"):
            backup = value
            value = []
            value.append(str(backup))
                                    
        self._value = value
        
    def show(self):
        '''The method is used to pretty print the attributes of the object.
        '''    
        print "========================================================="
        print "Parameter::" + self._parameter
        print "Value::" + str(self._value)
        print "Is Commented::" + str(self._iscommented)
        print "========================================================="

    def parameter(self):
        '''Returns name of the parameter.
        '''
        return self._parameter

    def iscommented(self):
        '''Tells whether the parameter is commented or not in the dot.conf
           file.
        '''
        return self._iscommented

    def value(self):
        '''Returns a list of all values for the given parameter.
        '''
        return self._value
    
    def position(self):
        '''Returns the byte position where the dot.conf parameter is located.

           <b><i>CAVEAT: Defined as None for all practical purposes. Meant for
                         internal use of python-dotconf</i></b>
        '''
        return self._position

    def compare(self, dotconf_object):
        '''Compares two conf_objects.

           Returns : True if the two objects match.
        '''
        if (self._parameter == dotconf_object.parameter() and
            self._iscommented == dotconf_object.iscommented() and
            self._value == dotconf_object.value()):
            return True
        else:
            return False

    def set_parameter(self, parameter):
        '''Set the parameter value for the dot.conf object.

           parameter : string
        '''
        self._parameter = parameter

    def set_iscommented(self, iscommented):
        '''Set the iscommented status for the dot.conf object.

           iscommented : boolean
        '''
        self._iscommented = iscommented

    def set_value(self, value):
        '''Set the value for the dot.conf parameter.

           value : List
        '''
        if(str(value.__class__.__name__ ) != "list"):
            backup = value
            value = []
            value.append(str(backup))
                                    
        self._value = value

            
class pydotconf:
    ''' This class will provide a very hackish interface to modify the 
        dot.conf configuration file.
    '''  

    def __init__(self, filename_path):
        '''filename_path    :   Absolute path name to the dot.conf file
           
	   Returns : object of class pydotconf.
        '''
        self._filehandle = open(filename_path, "r+")                
    
    def _search_parameter(self, parameter):
        '''This method will search for a particular dot.conf parameter and
           return the line number/ position from start.
        '''
        
        REGEX_VAL = "(?P<comment>#?)\s*" \
                    + "(?P<parameter>" \
                    + parameter \
                    + ")" \
                    + "\s*(?P<value>(-?\d+)|([\'\"].*))"

        pattern = re.compile(REGEX_VAL)
        byte_positions = [] 
                     
        self._filehandle.seek(0)
         
        last_position = self._filehandle.tell()
        line = self._filehandle.readline()        

        while (line != ""):
            search_result = pattern.search(line)
            if search_result:
                    
                if search_result.group('comment'):
                    x = conf_object(parameter, True,
                                   search_result.group('value'),
                                   last_position
                                   )
                    value_list = x.value()                                
                    value = self._get_parameter_value(value_list[0])
                    x._value = value
                    byte_positions.append(x)

                else:
                    x = conf_object(parameter, False,
                                   search_result.group('value'),
                                   last_position
                                   )
                    value_list = x.value()
                    value = self._get_parameter_value(value_list[0])
                    x._value = value
                    byte_positions.append(x)    
                   
            last_position = self._filehandle.tell()    
            line = self._filehandle.readline()
        
        return byte_positions

    def _get_parameter_value(self, line):
        '''This method returns the value of the parameter that we wish to 
           lookup.
        '''
        
        REGEX_VAL = "(?P<string>[\'\"][^\"\']*[\'\"])|(?P<integral>-?\d+)"

        pattern = re.compile(REGEX_VAL)
        
        # Split the line about the value
        line_split = pattern.finditer(line)
       
        # line_split is a list of re groups for each matched pattern in
        # the line.

        # ASSERTION : At any time in each group either the integral group
        #             will be set or the string group will be set. So the
	#             value can be pulled by checking one type for None.

        argument_list = [] 
        if line_split:
            try:
                while True:
                    argument_tuple = line_split.next().groups()
                    for item in argument_tuple:
                        if not item is None:
                            argument_list.append(item)
                                                    
            except StopIteration:
                return argument_list

    
    def _dotconf_to_string(self, dotconf_object):
        '''Converts a dotconf_object to a string which can be written
           to the dot.conf file.
        '''
        
        if dotconf_object.iscommented():
            entry = "# " + dotconf_object.parameter() + "\t"
        else:
            entry = dotconf_object.parameter() + "\t"

        for value in dotconf_object.value():
            entry = entry + str(value) + "\t"

        return (entry + "\n")
    
    def _add_parameter(self, dotconf_object):
        '''Helper function to add_parameter which writes a conf_object
           to the dot.conf file
        '''
        # The dot.conf entry will be written to the end of the file
        self._filehandle.seek(0, os.SEEK_END)
        
        text = self._dotconf_to_string(dotconf_object)
        self._filehandle.writelines(text)

    def _delete_parameter(self, position_set):
        '''Helper function to delete_parameter and delete_parameter_all
        '''
        self._filehandle.seek(0)
        write_back = []
        line = " "
        
        # Start reading the file one line at a time. If you reach a byte
        # position which must not be read, do not add it to the list of
        # lines to be written back.
        
        while (line != ""):
            if (not self._filehandle.tell() in position_set):
                line = self._filehandle.readline()
                write_back.append(line)
                    
            else:
                self._filehandle.readline()

        # Close the file in r+ mode and reopen in w mode so that the
        # entire file can be overwritten.
        file_name = self._filehandle.name
        self._filehandle.close()

        self._filehandle = open(file_name, "w")

        # Write the new data
        self._filehandle.seek(0)
        self._filehandle.writelines(write_back)
        self._filehandle.close()

        # Re-open file in r+ mode and return
        self._filehandle = open(file_name, "r+")
    
    def add_parameter(self, dotconf_objects):
        '''This method accepts a list of conf_object and searches for
           the <parameter,value> pair in the dot.conf file. If the entry
           is not found in the dot.conf file then the entry is made.

           Duplication is not allowed in the file, however, multiple
           entries for the same paramater are allowed.
           
           Returns : List of conf_object that are succesfully added to the
           dot.conf file
        '''                
        
        if(str(dotconf_objects.__class__.__name__ )== "conf_object"):
            object_copy = dotconf_objects
            dotconf_objects = [object_copy]
        
        objects_added = []
        for dotconf_object in dotconf_objects:
                
            search_items = self.get_parameter(dotconf_object.parameter())

            if search_items is None:
                self._add_parameter(dotconf_object)
                objects_added.append(dotconf_object)
                    
            else:
                dont_copy = False
                for item in search_items:
                    if item.compare(dotconf_object):
                        dont_copy = True

                # If we reach here, it means that the <parmater,value> was
                # not found, and we can add the entry to the file
                if not dont_copy:
                    self._add_parameter(dotconf_object)
                    objects_added.append(dotconf_object)

        return objects_added
                            
    def delete_parameter_all(self, parameter, delete_commented_also = False):
        '''Accepts a dot.conf parameter and deletes all uncommented
           instances of the parameter in the dot.conf file.
           
           If delete_commented_also is set to True, then commented
           values will also be deleted.
           
           Returns the list of dotconf_objects successfully deleted.
        '''

        objects_deleted = []
        search_items = self._search_parameter(parameter)
        position_set = Set()

        for item in search_items:
            # If we only want to delete uncommented entries
            if(not delete_commented_also):
                if not item.iscommented():
                    position_set.add(item.position())
                    objects_deleted.append(item)
            else:
                position_set.add(item.position())
                objects_deleted.append(item)
                        
        if len(position_set) > 0:
            self._delete_parameter(position_set)
            
        return objects_deleted

    def delete_parameter(self, dotconf_object):
        ''' Accepts a conf_object and deletes the specific entry in the
            dot.conf file if it exists.

            Returns :   True if the conf_object is found and succesfully
                        deleted.
        '''
        
        search_items = self._search_parameter(dotconf_object.parameter())
        position_set = Set()
        
        for item in search_items:
            if item.compare(dotconf_object):                                
                position_set.add(item.position())
                # ASSERTION : Duplicate entries are not possible so break
                # loop.
                break
                                         
        if len(position_set) > 0:
            self._delete_parameter(position_set)
            return True

        return False

    
    def get_parameter(self, parameter):
        '''This method accepts a parameter whose values we need to retrieve.
           The positions in the dot.conf file where the parameter is stored is
           retrieved and then for each position the values are retrieved.

           Returns : List of conf_object.
         '''
        
        #Find the parameter positions in the file   
        byte_positions = self._search_parameter(parameter)
        parameter_list = []

        for conf_objects in byte_positions:
            parameter_list.append(conf_object(conf_objects.parameter(),
                                             conf_objects.iscommented(),
                                             conf_objects.value())
                                  )                        
        return parameter_list
            
    def replace_parameter(self, oldConf, newConf):
        '''This accepts an old conf_object and replaces it with newConf.
           It is equivalent to modifying a parameter whose old value we
           are aware of.

           Returns : True if the replacement is made succesfully.
        '''
        if not self.delete_parameter(oldConf):
            return False
        
        par_added = self.add_parameter(newConf)
        
        return True
    
    def close(self):
        '''This method should be called to finally indicate to pyDotconf
           to close the open file.
        '''
        self._filehandle.close()
